1 /**
2  * This module implements functionality helpful for writing integration tests
3  * as opposed to the unit variety where unit-tests are defined as not
4  * having global side-effects. In constrast, this module implements
5  * assertions that check for global side-effects such as writing to the
6  * file system.
7  */
8 
9 module unit_threaded.integration;
10 
11 version (Windows) {
12     extern (C) int mkdir(char*);
13     extern (C) char* mktemp(char* template_);
14 
15     char* mkdtemp(char* t) {
16         version (unitUnthreaded)
17             return mkdtempImpl(t);
18         else {
19             synchronized {
20                 return mkdtempImpl(t);
21             }
22         }
23     }
24 
25     char* mkdtempImpl(char* t) {
26         char* result = mktemp(t);
27 
28         if (result is null)
29             return null;
30         if (mkdir(result))
31             return null;
32 
33         return result;
34     }
35 
36 } else {
37     extern (C) char* mkdtemp(char* template_);
38 }
39 
40 shared static this() {
41     import std.file : exists, rmdirRecurse;
42 
43     if (Sandbox.sandboxesPath.exists)
44         rmdirRecurse(Sandbox.sandboxesPath);
45 }
46 
47 @safe:
48 
49 /**
50  Responsible for creating a temporary directory to serve as a sandbox where
51  files can be created, written to or deleted.
52  */
53 struct Sandbox {
54     import std.path;
55 
56     enum defaultSandboxesPath = buildPath("tmp", "unit-threaded");
57     static string sandboxesPath = defaultSandboxesPath;
58     string testPath;
59 
60     /// Instantiate a Sandbox object
61     static Sandbox opCall() {
62         Sandbox ret;
63         ret.testPath = newTestDir;
64         return ret;
65     }
66 
67     static void setPath(string path) {
68         import std.file : exists, mkdirRecurse;
69 
70         sandboxesPath = path;
71         if (!sandboxesPath.exists)
72             () @trusted{ mkdirRecurse(sandboxesPath); }();
73     }
74 
75     static void resetPath() {
76         sandboxesPath = defaultSandboxesPath;
77     }
78 
79     /// Write a file to the sandbox
80     void writeFile(in string fileName, in string output = "") const {
81         import std.stdio : File;
82         import std.path : buildPath, dirName;
83         import std.file : mkdirRecurse;
84 
85         () @trusted{ mkdirRecurse(buildPath(testPath, fileName.dirName)); }();
86         File(buildPath(testPath, fileName), "w").writeln(output);
87     }
88 
89     /// Write a file to the sanbox
90     void writeFile(in string fileName, in string[] lines) const {
91         import std.array;
92 
93         writeFile(fileName, lines.join("\n"));
94     }
95 
96     /// Assert that a file exists in the sandbox
97     void shouldExist(string fileName, in string file = __FILE__, in size_t line = __LINE__) const {
98         import std.file : exists;
99         import std.path : buildPath;
100         import unit_threaded.should : fail;
101 
102         fileName = buildPath(testPath, fileName);
103         if (!fileName.exists)
104             fail("Expected " ~ fileName ~ " to exist but it didn't", file, line);
105     }
106 
107     /// Assert that a file does not exist in the sandbox
108     void shouldNotExist(string fileName, in string file = __FILE__, in size_t line = __LINE__) const {
109         import std.file : exists;
110         import std.path : buildPath;
111         import unit_threaded.should : fail;
112 
113         fileName = buildPath(testPath, fileName);
114         if (fileName.exists)
115             fail("Expected " ~ fileName ~ " to not exist but it did", file, line);
116     }
117 
118     /// read a file in the test sandbox and verify its contents
119     void shouldEqualContent(in string fileName, in string content,
120             in string file = __FILE__, in size_t line = __LINE__) const {
121         import std.file : readText;
122         import std..string : chomp, splitLines;
123         import unit_threaded.should : shouldEqual;
124 
125         readText(buildPath(testPath, fileName)).shouldEqual(content, file, line);
126     }
127 
128     /// read a file in the test sandbox and verify its contents
129     void shouldEqualLines(in string fileName, in string[] lines,
130             string file = __FILE__, size_t line = __LINE__) const {
131         import std.file : readText;
132         import std..string : chomp, splitLines;
133         import unit_threaded.should : shouldEqual;
134 
135         readText(buildPath(testPath, fileName)).chomp.splitLines.shouldEqual(lines, file, line);
136     }
137 
138     // `fileName` should contain `needle`
139     void fileShouldContain(in string fileName, in string needle,
140             in string file = __FILE__, in size_t line = __LINE__) {
141         import std.file : readText;
142         import unit_threaded.should : shouldBeIn;
143 
144         needle.shouldBeIn(readText(inSandboxPath(fileName)), file, line);
145     }
146 
147     string sandboxPath() @safe @nogc pure nothrow const {
148         return testPath;
149     }
150 
151     string inSandboxPath(in string fileName) @safe pure nothrow const {
152         import std.path : buildPath;
153 
154         return buildPath(sandboxPath, fileName);
155     }
156 
157     /**
158        Executing `args` should succeed
159      */
160     void shouldSucceed(string file = __FILE__, size_t line = __LINE__)(in string[] args...) @safe const {
161         import unit_threaded.should : UnitTestException;
162         import std.conv : text;
163         import std.array : join;
164 
165         const res = executeInSandbox(args);
166         if (res.status != 0)
167             throw new UnitTestException(text("Could not execute `",
168                     args.join(" "), "`:\n", res.output), file, line);
169     }
170 
171     alias shouldExecuteOk = shouldSucceed;
172 
173     /**
174        Executing `args` should fail
175      */
176     void shouldFail(string file = __FILE__, size_t line = __LINE__)(in string[] args...) @safe const {
177         import unit_threaded.should : UnitTestException;
178         import std.conv : text;
179         import std.array : join;
180 
181         const res = executeInSandbox(args);
182         if (res.status == 0)
183             throw new UnitTestException(text("`", args.join(" "),
184                     "` should have failed but didn't:\n", res.output), file, line);
185     }
186 
187 private:
188 
189     auto executeInSandbox(in string[] args) @safe const {
190         import std.process : execute, Config;
191         import std.algorithm : startsWith;
192         import std.array : replace;
193 
194         const string[string] env = null;
195         const config = Config.none;
196         const maxOutput = size_t.max;
197         const workDir = testPath;
198 
199         const executable = args[0].startsWith("./")
200             ? inSandboxPath(args[0].replace("./", "")) : args[0];
201 
202         return execute(executable ~ args[1 .. $], env, config, maxOutput, workDir);
203     }
204 
205     static string newTestDir() {
206         import std.file : exists, mkdirRecurse;
207 
208         if (!sandboxesPath.exists) {
209             () @trusted{ mkdirRecurse(sandboxesPath); }();
210         }
211 
212         return makeTempDir();
213     }
214 
215     static string makeTempDir() {
216         import std.algorithm : copy;
217         import std.exception : enforce;
218         import std.conv : to;
219         import std..string : fromStringz;
220         import core.stdc..string : strerror;
221         import core.stdc.errno : errno;
222 
223         char[2048] template_;
224         copy(buildPath(sandboxesPath, "XXXXXX") ~ '\0', template_[]);
225 
226         auto path = () @trusted{ return mkdtemp(&template_[0]).to!string; }();
227 
228         enforce(path != "",
229                 "\n" ~ "Failed to create temporary directory name using template '" ~ () @trusted{
230                     return fromStringz(&template_[0]);
231                 }() ~ "': " ~ () @trusted{ return strerror(errno).to!string; }());
232 
233         return path.absolutePath;
234     }
235 }